package callgraph;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map.Entry;

/**
 * As the name suggests, graphs can be written here. Directory delimiters are
 * windows style - sorry.
 */
public class GraphWriter {

	private static final String NEW_LINE = "\n";// System.getProperty("line.seperator");

	/**
	 * The different types of graph representations
	 */
	private static final int LG = 1;
	private static final int PAIRS = 2;
	private static final int WG2 = 3;

	/**
	 * The object of interest - gets written to disk.
	 */
	private AdjacenceList al;

	/**
	 * Must be a directory - no file extension. Not really pretty... But serves
	 * needs here well. Is used to build all output names. Serialized objects
	 * will be stored in filename + "\\data" LG in filename + '.lg'
	 */
	private String filename;

	/**
	 * The name that will be given to graphs. As JavaScript tests might serve as
	 * names (and those are in multiple directories), '\\' is replaced by '_',
	 * to exclude directory structures.
	 */
	private String graphname;

	/**
	 * 
	 * The type of Graph that will be generated from the serialized input File
	 * By default type == LG Type of .lg: type == 1 Type of .pairs: type == 2
	 */
	private int type = 1;

	/**
	 * Responsible for the text-graph files (.lg)
	 */
	private FileWriter fw;

	private BufferedWriter bw;

	/**
	 * Got a name for your output graph? Then start here. If called from the
	 * modified version of jsDriver.pl of this framework, the JavaScript
	 * filenames will end up here.
	 * 
	 * @param al
	 *            graph to be written
	 * @param filename
	 *            output directory
	 * @param graphname
	 *            name for the graph to be written
	 */
	public GraphWriter(AdjacenceList al, String filename, String graphname) {
		this(al, filename);
		this.graphname = graphname;
	}

	/**
	 * Got a name for your output graph? Then start here. If called from the
	 * modified version of jsDriver.pl of this framework, the JavaScript
	 * filenames will end up here.
	 * 
	 * @param al
	 *            graph to be written
	 * @param filename
	 *            output directory
	 * @param graphname
	 *            name for the graph to be written
	 * @param type
	 *            {lg|pairs} format of generated graph
	 */
	public GraphWriter(AdjacenceList al, String filename, String graphname, String type) {
		// this(al, filename);
		this.al = al;
		this.filename = filename;
		this.graphname = graphname;
		if (type.equalsIgnoreCase("pairs")) {
			this.type = PAIRS;
		} else if (type.equalsIgnoreCase("wg2")) {
			this.type = WG2;
		} else {
			this.type = LG;
		}

	}

	/**
	 * Leaves the graph name out - that will be set to "Interactive_SHELL_graph"
	 * 
	 * @param al
	 *            graph to be written
	 * @param filename
	 *            output directory
	 */
	public GraphWriter(AdjacenceList al, String filename) {
		this.al = al;
		this.filename = filename;
	}

	/**
	 * Writes {@link #al} as serialized object to disk. It will be placed in the
	 * directory {@link #filename}. And has {@link #graphname} + '.ser' as name.
	 */
	public void writeAdjacence() {
		String datafolder = filename;
		File f = new File(datafolder);
		f.mkdir();
		String name = "";
		if (graphname == null) {
			graphname = "Interactive_SHELL_graph";
			name = graphname;
		} else {
			name = graphname.replace('/', '_');
			name = name.replace('\\', '_');
		}
		String filename = datafolder + "\\" + name + ".ser";
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(filename);
			ObjectOutputStream o = new ObjectOutputStream(fos);
			o.writeObject(al);
		} catch (IOException e) {
			System.err.println(e);
		} finally {
			try {
				fos.close();
			} catch (Exception e) {
			}
		}
	}

	/**
	 * Writes the total reduced call-graph held in {@link #al} to file
	 * {@link #filename} + ".lg". As in this framework only total reduced graphs
	 * are used, {@link ArrayListSet} is used to generate a order preserving
	 * structure out of the Hash based AdjacenceList. The ordering is important,
	 * as the indices are used for the identification numbers in LG.
	 * 
	 * @param writeWeight
	 *            if set to true annotations will be written. Resulting in a
	 *            extended LG graph (see Matthias Huber for more information on
	 *            extended LG). Rarely needed (within current setup)
	 * @param skipDummies
	 *            set true if LG should contain no dummies
	 */
	public void writeR_w_total2lg(boolean writeWeight, boolean skipDummies, boolean skipJre, boolean skipSelfEdges) {
		setFileWriter();
		bw = new BufferedWriter(fw);
		writeGraphName2lg();
		String print;
		ArrayList vertices = vertexId(al, skipDummies, skipJre, skipSelfEdges);
		for (int i = 0; i < vertices.size(); i++) {
			Signature sig = (Signature) vertices.get(i);

			print = "v " + i + " " + sig.getSignature() + NEW_LINE;
			try {
				bw.write(print);
			} catch (IOException e) {
				System.out.println("I/O Exception: " + e.getMessage());
				System.exit(1);
			}
		}
		for (Iterator iter = al.iterator(); iter.hasNext();) {
			Entry entry = (Entry) iter.next();
			Signature sig = (Signature) entry.getKey();
			SuccessorList succ = (SuccessorList) entry.getValue();
			int startInd = vertices.indexOf(sig);
			String start = "e " + (startInd) + " ";
			if (startInd == -1)
				continue;
			if (succ == null)
				continue;
			for (int j = 0; j < succ.size(); j++) {
				AdjacenceElement ae = succ.getElement(j);
				Signature succsig = ae.getSignature();
				int end = vertices.indexOf(succsig);
				if (end == -1 || (skipSelfEdges && sig.equals(succsig)))
					continue;
				print = start + end + " call";
				if (writeWeight) {
					print += " ";
					int[] annot = ae.getAnnotations();
					for (int k = 0; k < annot.length; k++) {
						print += annot[k];
						if (k < annot.length - 1)
							print += ",";
					}
				}
				print += NEW_LINE;
				try {
					bw.write(print);
				} catch (IOException e) {
					System.out.println("I/O Exception: " + e.getMessage());
					System.exit(1);
				}
			}
		}
		try {
			bw.close();
		} catch (IOException e) {
			System.out.println("I/O Exception: " + e.getMessage());
			System.exit(1);
		}
	}

	public void writeR_w_total2wg2(boolean writeWeight, boolean skipDummies, boolean skipJre, boolean skipSelfEdges) {
		setFileWriter();
		bw = new BufferedWriter(fw);
		String end;
		String print = new String();
		ArrayList vertices = vertexId(al, skipDummies, skipJre, skipSelfEdges);
		for (Iterator iter = al.iterator(); iter.hasNext();) {
			print = new String();
			Entry entry = (Entry) iter.next();
			Signature sig = (Signature) entry.getKey();
			SuccessorList succ = (SuccessorList) entry.getValue();
			int startInd = vertices.indexOf(sig);
			int tmpSuccSize = succ.size();

			if (startInd == -1)
				continue;
			if (succ == null)
				continue;
			end = new String();
			for (int j = 0; j < succ.size(); j++) {
				AdjacenceElement ae = succ.getElement(j);
				Signature succsig = ae.getSignature();
				if (vertices.indexOf(succsig) != -1) {
					end = end + "(" + vertices.indexOf(succsig) + ", 1) ";
					//end = end + "(" + vertices.indexOf(succsig) + ", " + ae.getAnnotations()[0] + ") ";
				} else {
					tmpSuccSize--;
				}
				if (vertices.indexOf(succsig) == -1 || (skipSelfEdges && sig.equals(succsig)))
					continue;

				// if (writeWeight) {
				// print += " ";
				// int[] annot = ae.getAnnotations();
				// for (int k = 0; k < annot.length; k++) {
				// print += annot[k];
				// if (k < annot.length - 1)
				// print += ",";
				// }
				// }
				if (j == succ.size() - 1) {
					//print = (startInd) + " Knoten" +startInd+ ", name " + tmpSuccSize + " [ " + end + "]" + NEW_LINE;
					print = (startInd) + " " + ("knoten") + ", name " + tmpSuccSize + " [ " + end + "]" + NEW_LINE;
				}
			}
			try {
				bw.write(print);
			} catch (IOException e) {
				System.out.println("I/O Exception: " + e.getMessage());
				System.exit(1);
			}
		}
		try {
			bw.close();
		} catch (IOException e) {
			System.out.println("I/O Exception: " + e.getMessage());
			System.exit(1);
		}

	}

	/**
	 * Writes the total reduced call-graph held in {@link #al} to file
	 * {@link #filename} + ".pairs".
	 * 
	 * @param writeWeight
	 *            if set to true annotations will be written. Resulting in a
	 *            extended LG graph (see Matthias Huber for more information on
	 *            extended LG). Rarely needed (within current setup)
	 * @param skipDummies
	 *            set true if LG should contain no dummies
	 */
	public void writeR_w_total2pairs(boolean writeWeight, boolean skipDummies, boolean skipJre, boolean skipSelfEdges) {
		setFileWriter();
		bw = new BufferedWriter(fw);
		String print;
		ArrayList vertices = vertexId(al, skipDummies, skipJre, skipSelfEdges);
		for (Iterator iter = al.iterator(); iter.hasNext();) {
			Entry entry = (Entry) iter.next();
			Signature sig = (Signature) entry.getKey();
			SuccessorList succ = (SuccessorList) entry.getValue();
			int startInd = vertices.indexOf(sig);
			String start = (startInd) + " ";
			if (startInd == -1)
				continue;
			if (succ == null)
				continue;
			for (int j = 0; j < succ.size(); j++) {
				AdjacenceElement ae = succ.getElement(j);
				Signature succsig = ae.getSignature();
				int end = vertices.indexOf(succsig);
				if (end == -1 || (skipSelfEdges && sig.equals(succsig)))
					continue;
				print = start + end;
				if (writeWeight) {
					print += " ";
					int[] annot = ae.getAnnotations();
					for (int k = 0; k < annot.length; k++) {
						print += annot[k];
						if (k < annot.length - 1)
							print += ",";
					}
				}
				print += NEW_LINE;
				try {
					bw.write(print);
				} catch (IOException e) {
					System.out.println("I/O Exception: " + e.getMessage());
					System.exit(1);
				}
			}
		}
		try {
			bw.close();
		} catch (IOException e) {
			System.out.println("I/O Exception: " + e.getMessage());
			System.exit(1);
		}
	}

	/**
	 * To add an ID to each vertex (used as name within the DOT)
	 * 
	 * @param graph
	 *            our graph to print
	 * @param skipJre
	 * @param skipDummies
	 * @return signature -> ID
	 */
	private ArrayList vertexId(AdjacenceList graph, boolean skipDummies, boolean skipJre, boolean skipSelfEdges) {
		ArrayList out = new ArrayList();
		HashSet temp = new HashSet();
		for (Iterator iter = graph.iterator(); iter.hasNext();) {
			Entry entry = (Entry) iter.next();
			Signature vertex = (Signature) entry.getKey();
			SuccessorList succ = (SuccessorList) entry.getValue();
			if (temp.add(vertex)) {
				if (skipDummies && ((vertex.isDummy() && !vertex.isJre()) || onlyDummyPreccessors(vertex)))
					continue;
				if (skipJre && (vertex.isJre() || onlyJrePreccessors(vertex)))
					continue;
				if (skipSelfEdges && onlySelfEdge(vertex, succ))
					continue;
				out.add(vertex);
			}

			for (Iterator succIter = succ.iterator(); succIter.hasNext();) {
				AdjacenceElement ae = (AdjacenceElement) succIter.next();
				vertex = ae.getSignature();
				if (temp.add(vertex)) {
					if (skipDummies && ((vertex.isDummy() && !vertex.isJre()) || onlyDummyPreccessors(vertex)))
						continue;
					if (skipJre && (vertex.isJre() || onlyJrePreccessors(vertex)))
						continue;
					out.add(vertex);
				}
			}
		}
		return out;
	}

	private boolean onlySelfEdge(Signature sig, SuccessorList succ) {
		if (succ.size() != 1)
			return false;
		AdjacenceElement succsig = (AdjacenceElement) succ.get(0);
		if (!succsig.equals(sig))
			return false;
		return true;
	}

	private boolean onlyJrePreccessors(Signature sig) {
		for (Iterator iter = al.iterator(); iter.hasNext();) {
			Entry entry = (Entry) iter.next();
			Signature start = (Signature) entry.getKey();
			SuccessorList succ = (SuccessorList) entry.getValue();
			for (Iterator succIter = succ.iterator(); succIter.hasNext();) {
				AdjacenceElement end = (AdjacenceElement) succIter.next();
				if ((end.getSignature().equals(sig) && !start.isJre()) || (start.equals(sig) && !end.getSignature().isJre()))
					return false;
			}
		}
		return true;
	}

	private boolean onlyDummyPreccessors(Signature sig) {
		for (Iterator iter = al.iterator(); iter.hasNext();) {
			Entry entry = (Entry) iter.next();
			Signature start = (Signature) entry.getKey();
			SuccessorList succ = (SuccessorList) entry.getValue();
			for (Iterator succIter = succ.iterator(); succIter.hasNext();) {
				AdjacenceElement end = (AdjacenceElement) succIter.next();
				if ((end.getSignature().equals(sig) && !start.isDummy()) || (start.equals(sig) && !end.getSignature().isDummy()))
					return false;
			}
		}
		return true;
	}

	/**
	 * Helper for printing graph names within LG
	 */
	private void writeGraphName2lg() {
		String name;
		if (graphname == null) {
			// System.out.println("Writing to '" + filename + "'...");
			name = "t # Interactive_SHELL_graph" + NEW_LINE;
		} else {
			// System.out.println("Writing Test '" + graphname + "' to '" +
			// filename + "'...");
			name = "t # " + graphname + NEW_LINE;
		}
		try {
			bw.write(name);
		} catch (IOException e) {
			System.out.println("IO/Exception: " + e.getMessage());
			System.exit(1);
		}
	}

	/**
	 * Helper to initialize the {@link FileWriter}
	 */
	private void setFileWriter() {
		boolean apnd = false;
		File file;
		switch (this.type) {
		case PAIRS:
			file = new File(this.filename + ".pairs");
			break;
		case WG2:
			file = new File(this.filename + ".wg2");
			break;
		case LG:
			file = new File(this.filename + ".lg");
			break;
		default:
			file = new File(this.filename + ".lg");
		}

		if (file.exists())
			apnd = true;
		try {
			fw = new FileWriter(file, apnd);
		} catch (IOException e) {
			System.out.println("I/O Exception: " + e.getMessage());
		}
	}

}
